﻿// Copyright 2009 William P. Hickey Jr. and William U. Walker
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//     
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//  
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Linq;
using System.Xml.Serialization;
using System.ComponentModel;
using AgwpePort;
using AgwpePort.Aprs;

namespace CSReporter
{
    /// <summary>
    /// Storage and manipulation class for a single Community Status Report message.
    /// </summary>
    public class CSReportMessage : INotifyPropertyChanged
    {
        private byte csrMesssageType = 0;
        private string fromCall = "";
        private string station = "";
        private string statusCode = "";
        private DateTime timeHeard;
        private string detailText = "";
        private List<DetailTextData> detailList = new List<DetailTextData>();
        private int reportID = 0;
        private string incidentTime = "";
        private string streetNumber = "";
        private string streetDirection = "";
        private string streetName = "";
        private string cityCode = "";
        private string stateCode = "";
        private string latitude = "";
        private string longitude = "";
        private string crossRoad1 = "";
        private string crossRoad2 = "";
        private string aprsMsgNumber = "";
        private bool validReport = false;

        public event PropertyChangedEventHandler PropertyChanged;

        public byte MessageType
        {
            get { return csrMesssageType; }
            set { 
                csrMesssageType = value;
                this.NotifyPropertyChanged("MessageType");
            }
        }

        public int ReportID
        {
            get { return reportID; }
            set {
                if(value > 0 & value <= 9999)
                reportID = value;
                this.NotifyPropertyChanged("ReportID");
            }
        }
 
        public string FromCall
        {
            get { return fromCall; }
            set
            {
                fromCall = value == null ? null : value.Substring(0, Math.Min(value.Length, 10)).Trim().ToUpper();
                this.NotifyPropertyChanged("FromCall");
            }
        }

        public string Station
        {
            get { return station; }
            set {
                station = value == null ? null : value.Substring(0, Math.Min(value.Length, 10)).Trim().ToUpper();
                this.NotifyPropertyChanged("Station");
            }
        }   

        public string IncidentTime
        {
            get { return incidentTime; }
            set {
                incidentTime = value == null ? null : value.Substring(0, Math.Min(value.Length, 7)).Trim().ToLower();
                this.NotifyPropertyChanged("IncidentTime");
            }
        }   

        public string Latitude
        {
            get { return latitude; }
            set {
                latitude = value == null ? null : value.Substring(0, Math.Min(value.Length, 8)).Trim().ToUpper();
                this.NotifyPropertyChanged("Latitude");
            }
        }   

        public string Longitude
        {
            get { return longitude; }
            set {
                longitude = value == null ? null : value.Substring(0, Math.Min(value.Length, 9)).Trim().ToUpper();
                this.NotifyPropertyChanged("Longitude");
            }
        }   

        public string StatusCode
        {
            get { return statusCode; }
            set {
                statusCode = value == null ? null : value.Substring(0, Math.Min(value.Length, 22)).Trim().ToUpper();
                this.NotifyPropertyChanged("StatusCode");
            }
        }   

        public string CityCode
        {
            get { return cityCode; }
            set {
                cityCode = value == null ? null : value.Substring(0, Math.Min(value.Length, 3)).Trim().ToUpper();
                this.NotifyPropertyChanged("CityCode");
            }
        }   

        public string StateCode
        {
            get { return stateCode; }
            set {
                stateCode = value == null ? null : value.Substring(0, Math.Min(value.Length, 2)).Trim().ToUpper();
                this.NotifyPropertyChanged("StateCode");
            }
        }   

        public string StreetNumber
        {
            get { return streetNumber; }
            set {
                streetNumber = value == null ? null : value.Substring(0, Math.Min(value.Length, 10)).Trim();
                this.NotifyPropertyChanged("StreetNumber");
            }
        }   

        public string StreetDirection
        {
            get { return streetDirection; }
            set {
                streetDirection = value == null ? null : value.Substring(0, Math.Min(value.Length, 1)).Trim().ToUpper();
                this.NotifyPropertyChanged("StreetDirection");
            }
        }   

        public string StreetName
        {
            get { return streetName; }
            set {
                streetName = value == null ? null : value.Substring(0, Math.Min(value.Length, 25)).Trim();
                this.NotifyPropertyChanged("StreetName");
            }
        }   

        public string CrossRoad1
        {
            get { return crossRoad1; }
            set {
                crossRoad1 = value == null ? null : value.Substring(0, Math.Min(value.Length, 25)).Trim();
                this.NotifyPropertyChanged("CrossRoad1");
            }
        }   

        public string CrossRoad2
        {
            get { return crossRoad2; }
            set {
                crossRoad2 = value == null ? null : value.Substring(0, Math.Min(value.Length, 25)).Trim();
                this.NotifyPropertyChanged("CrossRoad2");
            }
        }   

        public string DetailText
        {
            get {
                string retval = "";

                var res = from row in detailList
                          orderby row.AprsMsgNumber
                          select row;
                foreach (DetailTextData detail in res)
                    retval += detail.DetailText;

                return retval; 
            }
            set {
                detailText = value == null ? null : value.Substring(0, Math.Min(value.Length, 51));
                this.NotifyPropertyChanged("DetailText");
            }
        }

        public List<DetailTextData> DetailList
        {
            get { return detailList; }
            set { 
                detailList = value;
                this.NotifyPropertyChanged("DetailList");
            }
        }

        public DateTime TimeHeard
        {
            get { return timeHeard; }
            set { 
                timeHeard = value;
                this.NotifyPropertyChanged("TimeHeard");
            }
        }   

        public string AprsMsgNumber
        {
            get { return aprsMsgNumber; }
            set {
                aprsMsgNumber = value == null ? null : value.Substring(0, Math.Min(value.Length, 5)).Trim().ToUpper();
                this.NotifyPropertyChanged("AprsMsgNumber");
            }
        }   

        public bool IsValidReport
        {
            get { return validReport; }
        }

        public CSReportMessage()
        {}

        public CSReportMessage(string fromCall, AprsData packet, DateTime timeHeard)
        {
            if (fromCall != null & packet != null & timeHeard != null)
            {
                this.timeHeard = timeHeard;
                this.AprsMsgNumber = packet.MsgNumber;
                this.FromCall = fromCall;
                validReport = Parse(ASCIIEncoding.UTF8.GetBytes(packet.MsgText));
            }
        }

        public CSReportMessage(string fromCall, byte[] messageData, DateTime timeHeard, string aprsMsgNumber)
        {
            if (fromCall != null & messageData != null & timeHeard != null & aprsMsgNumber != null)
            {
                this.timeHeard = timeHeard;
                this.AprsMsgNumber = aprsMsgNumber;
                this.FromCall = fromCall;
                validReport = Parse(messageData);
            }
        }

        private void NotifyPropertyChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }

        private bool Parse(byte[] data)
        {
            // Make sure we have at least a minimum length information array.
            if (data.Length >= 16)
            {
                // Make sure the first character in the message is a '^', the next character is
                // 1, 2, 3 or C and that the APRS packet has a message number. 
                // This is the basic signature to indicate the message is a community status report.
                if (data[0] == '^' & (data[1] == '1' | data[1] == '2' | data[1] == '3' | data[1] == 'C') & aprsMsgNumber.Trim().Length > 0)
                {
                    // Record the status report message type, station and reportId. These items 
                    // will be present in all message types.
                    csrMesssageType = data[1];
                    this.Station = ASCIIEncoding.UTF8.GetString(data, 2, 10).Trim().ToUpper();
                    // Parse the reportID and ensure it is an integer value...
                    if(!int.TryParse(ASCIIEncoding.UTF8.GetString(data, 12, 4),out reportID))
                        // If not, this is not a valid community status report message.
                        return (false); ;

                    switch (csrMesssageType)
                    {
                        // Base message containing the incident time, latitude, longitude, status code, 
                        // city code and state code.
                        case (byte)'1':
                            // Make sure the array is the correct length.
                            // The length will vary depending on whether or not the city code and/or state code were sent.
                            if (data.Length == 67)
                            {
                                this.IncidentTime = ASCIIEncoding.UTF8.GetString(data, 16, 7);
                                this.Latitude = ASCIIEncoding.UTF8.GetString(data, 23, 8);
                                this.Longitude = ASCIIEncoding.UTF8.GetString(data, 31, 9);
                                this.StatusCode = ASCIIEncoding.UTF8.GetString(data, 40, 22).Trim().ToUpper();
                                this.CityCode = ASCIIEncoding.UTF8.GetString(data, 62, 3).Trim().ToUpper();
                                this.StateCode = ASCIIEncoding.UTF8.GetString(data, 65, 2).Trim().ToUpper();
                            }
                            else
                                // If the array is not 66 bytes, this is not a valid community status report message.
                                return (false);
                            break;
                        // Street address information.
                        case (byte)'2':
                            // Make sure we have at least a minimum length information array and the street 
                            // information contains 3 delimited strings.
                            if (data.Length >= 18 & data.Length <= 67 & ASCIIEncoding.UTF8.GetString(data, 16, data.Length - 16).Split(',').Length == 3)
                            {
                                this.StreetNumber = ASCIIEncoding.UTF8.GetString(data, 16, data.Length - 16).Split(',')[0].Trim();
                                this.StreetDirection = ASCIIEncoding.UTF8.GetString(data, 16, data.Length - 16).Split(',')[1].Trim().ToUpper();
                                this.StreetName = ASCIIEncoding.UTF8.GetString(data, 16, data.Length - 16).Split(',')[2].Trim();
                                // If any of the strings are too long...
                                if (streetNumber.Length > 10 | streetDirection.Length > 1 | streetName.Length > 25)
                                    // This is not a valid community status report message.
                                    return (false);
                            }
                            else
                                // This is not a valid community status report message.
                                return (false);
                            break;
                        // Major intersection information.
                        case (byte)'3':
                            // Make sure we have at least a minimum length information array and the street intersection 
                            // information contains 2 delimited strings.
                            if (data.Length >= 17 & data.Length <= 67 & ASCIIEncoding.UTF8.GetString(data, 16, data.Length - 16).Split(',').Length == 2)
                            {
                                this.CrossRoad1 = ASCIIEncoding.UTF8.GetString(data, 16, data.Length - 16).Split(',')[0].Trim();
                                this.CrossRoad2 = ASCIIEncoding.UTF8.GetString(data, 16, data.Length - 16).Split(',')[1].Trim();
                                // If ether of the strings are too long...
                                if (crossRoad1.Length > 25 | crossRoad2.Length > 25)
                                    // This is not a valid community status report message.
                                    return (false);
                            }
                            else
                                // This is not a valid community status report message.
                                return (false);
                            break;
                        // Status report details.
                        case (byte)'C':
                            // Make sure we have at least a minimum length information array.
                            if (data.Length >= 16 & data.Length <= 67)
                            {
                                this.detailList.Add(new DetailTextData(ASCIIEncoding.UTF8.GetString(data, 16, data.Length - 16), aprsMsgNumber));
                            }
                            else
                                // This is not a valid community status report message.
                                return (false);
                            break;
                    }
                    // The messsage has been parsed successfully.
                    return (true);
                }
                else
                    // This is not a valid community status report message, return false.
                    return (false);
            }
            // This is not a valid community status report message, return false.
            return (false);
        }
    }

    /// <summary>
    /// Storage and manipulation class for a single Community Status Report message's detail text.
    /// </summary>
    public class DetailTextData
    {
        private string detailText = "";
        private string aprsMsgNumber = "";

        public string DetailText
        {
            get { return detailText; }
            set { detailText = value; }
        }

        public string AprsMsgNumber
        {
            get { return aprsMsgNumber; }
            set {
                aprsMsgNumber = value == null ? null : value.Substring(0, Math.Min(value.Length, 5)).Trim().ToUpper();
            }
        }

        public DetailTextData()
        { }

        public DetailTextData(string detailText, string aprsMsgNumber)
        {
            this.DetailText = detailText;
            this.AprsMsgNumber = aprsMsgNumber;
        }
    }

    /// <summary>
    /// Storage and manipulation class for a List of CSReportMessage objects.
    /// </summary>
    public class CSReportList : BindingList<CSReportMessage>
    {
        // Maximum message age in minutes. (1440 = 24 hours)
        private int maxAge = 1440;

        public int MaxAge
        {
            get { return maxAge; }
            set { maxAge = value >= 0 ? value : 0; }
        }

        public CSReportList() { }

        public CSReportList(MessageList messages)
        {
            Parse(messages, maxAge);
        }

        public CSReportList(MessageList messages, int maximumAge)
        {
            Parse(messages, maximumAge);
        }

        public void Parse(MessageList messages, int maximumAge)
        {
            // Set the maxAge value;
            MaxAge = maximumAge;

            foreach (AprsMessage aprsMsg in messages)
            {
                if (aprsMsg.IsCSR & aprsMsg.IsActive)
                {
                    CSReportMessage csReportMsg = new CSReportMessage(aprsMsg.FromCall, ASCIIEncoding.UTF8.GetBytes(aprsMsg.Message.PadRight(67, ' ')), aprsMsg.TimeHeard, aprsMsg.AprsMsgNumber);
                    if ((csReportMsg.IsValidReport & maxAge > 0 ? aprsMsg.TimeHeard >= DateTime.Now.AddMinutes(-maxAge) : true))
                        AddCSReport(csReportMsg);
                }
            }
        }

        public int GetMaxID(string callSign)
        {
            int retval = 0;

            foreach (CSReportMessage message in Items)
            {
                if (message.FromCall == callSign & message.TimeHeard >= DateTime.Now.AddMinutes(-1440))
                    retval = Math.Max(retval, message.ReportID);
            }
            return retval;
        }

        public double GetMidLat()
        {
            double max = 0;
            double min = 0;
            double latitude = 0;
            int count = 0;

            foreach (CSReportMessage message in Items)
            {
                count++;
                latitude = AprsCalc.LatLonToDouble(message.Latitude);
                if (count == 1)
                {
                    max = latitude;
                    min = latitude;
                }
                else
                {
                    max = Math.Max(max, latitude);
                    min = Math.Min(min, latitude);
                }
            }
            return min + ((max - min) / 2);
        }

        public double GetMidLon()
        {
            double max = 0;
            double min = 0;
            double longitude = 0;
            int count = 0;

            foreach (CSReportMessage message in Items)
            {
                count++;
                longitude = AprsCalc.LatLonToDouble(message.Longitude);
                if (count == 1)
                {
                    max = longitude;
                    min = longitude;
                }
                else
                {
                    max = Math.Max(max, longitude);
                    min = Math.Min(min, longitude);
                }
            }
            return min + ((max - min) / 2);
        }

        /// <summary>
        /// Adds a CSReportMessage object to the end of the List<CSReportMessage> collection if the 
        /// Station and ReportID properties are unique to the new CSReportMessage object. 
        /// If these properties match an existing record, the existing record is updated based on the 
        /// MessageType property of the new CSReportMessage object.
        /// </summary>
        public void AddCSReport(CSReportMessage csReportMessage)
        {
            bool found = false;

            if (Items.Count > 0)
            {
                foreach (CSReportMessage message in Items)
                {
                    if (message.Station == csReportMessage.Station & message.ReportID == csReportMessage.ReportID & 
                        message.TimeHeard <= csReportMessage.TimeHeard.AddSeconds(20) &
                        message.TimeHeard >= csReportMessage.TimeHeard.AddSeconds(-20))
                    {
                        switch (csReportMessage.MessageType)
                        {
                            case (byte)'1':
                                message.TimeHeard = csReportMessage.TimeHeard;
                                message.Latitude = csReportMessage.Latitude;
                                message.Longitude = csReportMessage.Longitude;
                                message.StatusCode = csReportMessage.StatusCode;
                                message.CityCode = csReportMessage.CityCode;
                                message.StateCode = csReportMessage.StateCode;
                                break;
                            case (byte)'2':
                                message.StreetNumber = csReportMessage.StreetNumber;
                                message.StreetDirection = csReportMessage.StreetDirection;
                                message.StreetName = csReportMessage.StreetName;
                                break;
                            case (byte)'3':
                                message.CrossRoad1 = csReportMessage.CrossRoad1;
                                message.CrossRoad2 = csReportMessage.CrossRoad2;
                                break;
                            case (byte)'C':
                                message.DetailList.Add(new DetailTextData(csReportMessage.DetailText, csReportMessage.AprsMsgNumber));
                                break;
                        }
                        found = true;
                    }
                }
            }
            if (!found)
            {
                Add(csReportMessage);
            }
        }

        public void ReadXml(string fileName)
        {
            if (File.Exists(fileName))
            {
                Clear();
                XmlSerializer deserializer = new XmlSerializer(typeof(List<CSReportMessage>));
                using (TextReader textReader = new StreamReader(fileName))
                    foreach (CSReportMessage ic in (List<CSReportMessage>)deserializer.Deserialize(textReader))
                        AddCSReport(ic);
            }
        }

        public void WriteXml(string fileName)
        {
            if (Directory.GetParent(fileName).Exists)
            {
                XmlSerializer serializer = new XmlSerializer(typeof(List<CSReportMessage>));
                using (TextWriter textWriter = new StreamWriter(fileName))
                    serializer.Serialize(textWriter, Items);
            }
        }
    }
}
